package org.parameterchecker;

import java.util.Collection;
import java.util.Map;

/**
 * Utility methods for doing various common checks to method parameters, and throwing IllegalArgumentException if necessary.
 *
 * @author Hans Haggstrom
 */
public final class Check
{

    /**
     * Checks that the specified parameter is not null.
     *
     * @param parameter     the parameter value to check
     * @param parameterName the name of the parameter (used in error messages)
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void notNull( final Object parameter, final String parameterName )
    {
        if ( parameter == null )
        {
            throwIllegalArgumentException( parameterName, parameter, "not be null" );
        }
    }


    /**
     * Checks that the specified parameter is not infinite and not NaN (not a number).
     *
     * @param parameter     the parameter value to check
     * @param parameterName the name of the parameter (used in error messages)
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void normalNumber( double parameter, String parameterName )
    {
        if ( Double.isInfinite( parameter ) )
        {
            throwIllegalArgumentException( parameterName, parameter, "be a normal number" );
        }
        if ( Double.isNaN( parameter ) )
        {
            throwIllegalArgumentException( parameterName, parameter, "be a normal number" );
        }
    }


    /**
     * Checks that the specified parameter is not NaN (not a number).  It can still be infinite.
     *
     * @param parameter     the parameter value to check
     * @param parameterName the name of the parameter (used in error messages)
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void notNaN( double parameter, String parameterName )
    {
        if ( Double.isNaN( parameter ) )
        {
            throwIllegalArgumentException( parameterName, parameter, "not be NaN" );
        }
    }


    /**
     * Checks that the specified parameter is positive and not infinite and not NaN (not a number).
     *
     * @param parameter     the parameter value to check
     * @param parameterName the name of the parameter (used in error messages)
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void positiveOrZero( double parameter, String parameterName )
    {
        normalNumber( parameter, parameterName );

        if ( parameter < 0 )
        {
            throwIllegalArgumentException( parameterName, parameter, "be a normal positive number" );
        }
    }


    /**
     * Checks that the specified parameter is positive or zero.
     *
     * @param parameter     the parameter value to check
     * @param parameterName the name of the parameter (used in error messages)
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void positiveOrZero( long parameter, String parameterName )
    {
        if ( parameter < 0 )
        {
            throwIllegalArgumentException( parameterName, parameter, "be a positive or zero number" );
        }
    }


    /**
     * Checks that the specified parameter is positive, not zero, not infinite and not NaN (not a number).
     *
     * @param parameter     the parameter value to check
     * @param parameterName the name of the parameter (used in error messages)
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void positiveNonZero( double parameter, String parameterName )
    {
        normalNumber( parameter, parameterName );

        if ( parameter <= 0 )
        {
            throwIllegalArgumentException( parameterName, parameter, "be a normal positive non zero number" );
        }
    }


    /**
     * Checks that the specified parameter is positive and not zero.
     *
     * @param parameter     the parameter value to check
     * @param parameterName the name of the parameter (used in error messages)
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void positiveNonZero( long parameter, String parameterName )
    {
        if ( parameter <= 0 )
        {
            throwIllegalArgumentException( parameterName, parameter, "be a positive non zero number" );
        }
    }


    /**
     * Checks that the specified parameter is between 0 and 1 (inclusive).
     *
     * @param parameter     the parameter value to check
     * @param parameterName the name of the parameter (used in error messages)
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void zeroToOneInclusive( final double parameter, String parameterName )
    {
        normalNumber( parameter, parameterName );

        if ( parameter < 0 || parameter > 1 )
        {
            throwIllegalArgumentException( parameterName, parameter, "be in the range 0 to 1 inclusive" );
        }
    }


    /**
     * Checks that the specified parameter is in the specified range .
     *
     * @param parameter             the parameter value to check
     * @param parameterName         the name of the parameter (used in error messages)
     * @param minimumValueInclusize lower end of the bound, the value can be equal to this.
     * @param maximumValueExclusive upper end of the bound, the value should be smaller than this.
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void inRange( final double parameter,
                                String parameterName,
                                double minimumValueInclusize,
                                double maximumValueExclusive )
    {
        normalNumber( parameter, parameterName );

        if ( parameter < minimumValueInclusize || parameter >= maximumValueExclusive )
        {
            throwIllegalArgumentException( parameterName,
                                           parameter,
                                           "be in the range " + minimumValueInclusize + " (inclusive) to " + maximumValueExclusive + " (exclusive)" );
        }
    }


    /**
     * Checks that the specified parameter is in the specified range .
     *
     * @param parameter             the parameter value to check
     * @param parameterName         the name of the parameter (used in error messages)
     * @param minimumValueInclusize lower end of the bound, the value can be equal to this.
     * @param maximumValueExclusive upper end of the bound, the value should be smaller than this.
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void inRange( final int parameter,
                                String parameterName,
                                int minimumValueInclusize,
                                int maximumValueExclusive )
    {
        if ( parameter < minimumValueInclusize || parameter >= maximumValueExclusive )
        {
            throwIllegalArgumentException( parameterName,
                                           parameter,
                                           "be in the range " + minimumValueInclusize + " (inclusive) to " + maximumValueExclusive + " (exclusive)" );
        }
    }


    /**
     * Checks that the specified parameter is in the specified range.
     *
     * @param parameter             the parameter value to check
     * @param parameterName         the name of the parameter (used in error messages)
     * @param minimumValueInclusize lower end of the bound, the value can be equal to this.
     * @param maximumValueInclusive upper end of the bound, the value can be equal to this.
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void inRangeInclusive( final double parameter,
                                         String parameterName,
                                         double minimumValueInclusize,
                                         double maximumValueInclusive )
    {
        normalNumber( parameter, parameterName );

        if ( parameter < minimumValueInclusize || parameter > maximumValueInclusive )
        {
            throwIllegalArgumentException( parameterName,
                                           parameter,
                                           "be in the range " + minimumValueInclusize + " (inclusive) to " + maximumValueInclusive + " (inclusive)" );
        }
    }


    /**
     * Checks that the specified parameter is larger than or equal to the value.
     *
     * @param parameter             the parameter value to check
     * @param parameterName         the name of the parameter (used in error messages)
     * @param minimumValueInclusize lower end of the bound, the value can be equal to this.
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void equalsOrLargerThan( final int parameter,
                                           String parameterName,
                                           int minimumValueInclusize )
    {
        if ( parameter < minimumValueInclusize )
        {
            throwIllegalArgumentException( parameterName, parameter, "be larger or equal to " + minimumValueInclusize );
        }
    }

    /**
     * Checks that the specified parameter is larger than or equal to the value.
     *
     * @param parameter             the parameter value to check
     * @param parameterName         the name of the parameter (used in error messages)
     * @param minimumValueInclusize lower end of the bound, the value can be equal to this.
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void equalsOrLargerThan( final double parameter,
                                           String parameterName,
                                           int minimumValueInclusize )
    {
        normalNumber( parameter, parameterName );

        if ( parameter < minimumValueInclusize )
        {
            throwIllegalArgumentException( parameterName, parameter, "be larger or equal to " + minimumValueInclusize );
        }
    }

    /**
     * Checks that the specified parameter is larger than the specified value.
     *
     * @param parameter             the parameter value to check
     * @param parameterName         the name of the parameter (used in error messages)
     * @param minimumValueExclusive the value that the parameter should be larger than.
     * @param thresholdName         the name of the lower bound.
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void largerThan( final double parameter,
                                   String parameterName,
                                   double minimumValueExclusive,
                                   String thresholdName )
    {
        if ( parameter <= minimumValueExclusive )
        {
            throwIllegalArgumentException( parameterName,
                                           parameter,
                                           "be larger than " + thresholdName + " (" + minimumValueExclusive + ")" );
        }
    }


    /**
     * Checks that the specified element is not in the specified container.
     *
     * @param element        the parameter value to check
     * @param collection     the collection to check that the element is not in.
     * @param collectionName the name of the collection (used in error messages)
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void notAlreadyContained( final Object element,
                                            final Collection collection,
                                            final String collectionName )
    {
        if ( collection.contains( element ) )
        {
            throw new IllegalArgumentException( "The " + collectionName + " already contains " + describeElementType( element ) );
        }
    }


    /**
     * Checks that the specified key is not in the specified map.
     *
     * @param key     the parameter value to check
     * @param map     the map to check that the key is not in.
     * @param mapName the name of the map (used in error messages)
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void notAlreadyContained( final Object key, final Map map, final String mapName )
    {
        if ( map.containsKey( key ) )
        {
            throw new IllegalArgumentException( "The " + mapName + " already contains the key '" + key + "'" );
        }
    }


    /**
     * Checks that the specified element is in the specified container.
     *
     * @param element        the parameter value to check
     * @param collection     the collection to check that the element is in.
     * @param collectionName the name of the collection (used in error messages)
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void contained( final Object element,
                                  final Collection collection,
                                  final String collectionName )
    {
        if ( !collection.contains( element ) )
        {
            throw new IllegalArgumentException( "The " + collectionName + " doesn't contain " + describeElementType(
                    element ) );
        }
    }


    /**
     * Checks that the specified parameter is a Java style identifier (starts with letter, contains only letters, numbers, and underscores.
     *
     * @param parameter     the parameter value to check
     * @param parameterName the name of the parameter (used in error messages)
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void isIdentifier( final String parameter, final String parameterName )
    {
        notNull( parameter, parameterName );

        if ( !isIdentifier( parameter ) )
        {
            throwIllegalArgumentException( parameterName,
                                           parameter,
                                           "be a valid identifier (start with letter and contain only letters, numbers, and underscores)" );
        }
    }


    /**
     * Checks that the specified parameter is of the specified type.
     *
     * @param parameter             the parameter value to check
     * @param parameterName         the name of the parameter (used in error messages)
     * @param expectedParameterType the type that the parameter should be of
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void isInstanceOf( final Object parameter,
                                     final String parameterName,
                                     final Class expectedParameterType )
    {
        notNull( parameter, parameterName );

        if ( !expectedParameterType.isInstance( parameter ) )
        {
            throwIllegalArgumentException( parameterName,
                                           "of type '" + parameter.getClass() + "'",
                                           "be of type '" + expectedParameterType + "'" );
        }
    }


    /**
     * Throws an exception if the specified size of a parameter array is not equal to the expected size.
     */
    public static void arrayLength( final int parameterArrayLength,
                                    final String parameterArrayName,
                                    final int expectedLength )
    {
        positiveOrZero( expectedLength, "expectedLength" );

        if ( parameterArrayLength != expectedLength )
        {
            throwIllegalArgumentException( parameterArrayName,
                                           "of length " + parameterArrayLength,
                                           "have the length " + expectedLength );
        }
    }


    /**
     * Checks that two parameters are not equal.
     *
     * @param firstParameter      the first parameter value to check
     * @param firstParameterName  the name of the first parameter (used in error messages)
     * @param secondParameter     the second parameter value to check
     * @param secondParameterName the name of the seconds parameter (used in error messages)
     *
     * @throws IllegalArgumentException if the check fails.
     */
    public static void notEqual( final Object firstParameter, final Object secondParameter,
                                 final String firstParameterName, final String secondParameterName )
    {
        //noinspection OverlyComplexBooleanExpression
        if ( ( firstParameter == null && secondParameter == null ) ||
             ( firstParameter != null && firstParameter.equals( secondParameter ) ) ||
             ( secondParameter != null && secondParameter.equals( firstParameter ) ) )
        {
            throw new IllegalArgumentException(
                    "The parameters '" + firstParameterName + "' and '" + secondParameterName +
                    "' should not be equal, but they were '" + firstParameter + "' and '" + secondParameter + "'." );
        }
    }


    /**
     * Checks that the specified collection is not null and not empty.
     *
     * @param collection the collection to test
     * @param name       a name to use for the collection in exception messages.
     *
     * @throws IllegalArgumentException if the collection is null or empty.
     */
    public static void notEmpty( final Collection collection, final String name )
            throws IllegalArgumentException
    {
        notNull( collection, "collection" );

        if ( collection.isEmpty() )
        {
            throwIllegalArgumentException( name, collection, "not be empty" );
        }
    }

    //======================================================================
    // Private Methods

    /**
     * @param identifier the parameter to check.
     *
     * @return true if the specified string is a Java identifier as specified by the Character methods, false if not.
     */
    private static boolean isIdentifier( String identifier )
    {
        if ( identifier == null )
        {
            return false;
        }

        if ( identifier.length() == 0 )
        {
            return false;
        }

        // Test initial character
        if ( !Character.isJavaIdentifierStart( identifier.charAt( 0 ) ) )
        {
            return false;
        }

        // Test each contained character
        for ( int i = 1; i < identifier.length(); i++ )
        {
            if ( !Character.isJavaIdentifierPart( identifier.charAt( i ) ) )
            {
                return false;
            }
        }

        // The string passed the test
        return true;
    }

    private Check()
    {
    }


    private static void throwIllegalArgumentException( final String parameterName,
                                                       final Object parameter,
                                                       final String expectedCondition )
    {
        throw new IllegalArgumentException(
                "The parameter '" + parameterName + "' should " + expectedCondition + ", but it was '" + parameter + "'." );
    }


    private static String describeElementType( final Object element )
    {
        final String elementDesc;
        if ( element == null )
        {
            elementDesc = "a null element.";
        }
        else
        {
            elementDesc = "the " + element.getClass().getName() + "  '" + element + "'.";
        }

        return elementDesc;
    }

}
